package com.bobo.base.auth;

import com.alibaba.fastjson.JSONObject;
import com.bobo.util.JWTUtil;
import com.bobo.util.JedisUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;

import java.util.HashMap;
import java.util.Map;

/**
 * JWT身份验证
 *
 * @author LILIBO
 * @since 2021-06-24
 */
@Slf4j
public class JWTAuth {

    /**
     * Redis存储用户Token前缀（接UID）
     */
    public static final String KEY_REDIS_AUTH_USER = "auth:user:";

    /**
     * 有效期（单位：秒）
     */
    public static final Long JWT_TTL = 60 * 60 * 1000L; // 一个小时

    /**
     * 生成JWT（用户登录成功后生成JWT，保存用户ID，并将用户信息存储到Redis中）
     * <p>
     * 使用HS256算法，生成签名的时候使用的秘钥secret,这个方法本地封装了的，一般可以从本地配置文件中读取，切记这个秘钥不能外露。它就是你服务端的私钥，在任何场景都不应该流露出去。一旦客户端得知这个Secret, 那就意味着客户端是可以自我签发JWT了
     *
     * @param securityUser 授权用户对象（Security中实现了UserDetails接口的类型）
     * @return
     */
    public static String generateJWT(SecurityUser securityUser) {
        String uid = securityUser.getSysUser().getId().toString();
        String token = JWTUtil.createJWT(uid, JWTAuth.JWT_TTL);
        // 存放于Redis中的Key
        String redisKey = JWTAuth.KEY_REDIS_AUTH_USER + uid;

        // 从资源池中获取jedis对象用于操作Redis
        Jedis jedis = JedisUtil.getJedis();
        try {
            // Token存放Redis中，并设置失效时间
            if (jedis != null) {
                jedis.setex(redisKey, JWTAuth.JWT_TTL, JSONObject.toJSONString(securityUser));
            }
        } catch (RuntimeException e) {
            e.printStackTrace();
        } finally {
            // 用完后将jedis对象返回资源池中
            JedisUtil.close(jedis);
        }
        return token;
    }

    /**
     * 生成JWT（用户登录成功后生成JWT，保存用户简要信息）
     * <p>
     * 使用HS256算法，生成签名的时候使用的秘钥secret,这个方法本地封装了的，一般可以从本地配置文件中读取，切记这个秘钥不能外露。它就是你服务端的私钥，在任何场景都不应该流露出去。一旦客户端得知这个Secret, 那就意味着客户端是可以自我签发JWT了
     *
     * @param params 用户数据（存放用户数据）
     * @return
     */
    public static String generateJWT(Map<String, Object> params) {
        String uid = params.get("subject").toString();
        String token = JWTUtil.createJWT(uid, JWTAuth.JWT_TTL);
        // 存放于Redis中的Key
        String redisKey = JWTAuth.KEY_REDIS_AUTH_USER + uid;

        // 从资源池中获取jedis对象用于操作Redis
        Jedis jedis = JedisUtil.getJedis();
        try {
            // Token存放Redis中，并设置失效时间
            jedis.setex(redisKey, JWTAuth.JWT_TTL, JSONObject.toJSONString(params));
        } catch (RuntimeException e) {
            e.printStackTrace();
        } finally {
            // 用完后将jedis对象返回资源池中
            JedisUtil.close(jedis);
        }
        return token;
    }

    /**
     * 解析JWT
     *
     * @param token 加密后的Token字符串
     * @return JWT对象中的数据
     */
    public static Claims parseJWT(String token) {
        // 获取到Claims后可以从中取得Subject及其他参数
        return JWTUtil.parseJWT(token);
    }

    /**
     * 解析JWT中的Subject
     *
     * @param token 加密后的Token字符串
     * @return JWT对象中的Subject数据
     */
    public static String parseJWTSubject(String token) {
        // 获取到Claims后可以从中取得Subject及其他参数
        return JWTUtil.parseJWT(token).getSubject();
    }

    /**
     * 验证JWT
     * <p>
     * 流程参考：
     * 1.将传过来的Token解析，取出subject中存储的用户ID
     * 2.根据用户ID获取Redis中存储的用户信息，如果不存在，提示：用户验证失败
     * 3.如果存在则获取用户信息及权限列表，判断是否有权限访问业务方法
     */
    public static boolean verifyJWT(String token) {
        try {
            // 解析JWT
            Claims claims = JWTUtil.parseJWT(token);
            // 获取Subject（UID）
            String uid = claims.getSubject();

            // 验证Redis中是否存在该Key
            String redisKey = JWTAuth.KEY_REDIS_AUTH_USER + uid;

            // Redis中的key剩余时间
            long expire = 0;
            // 从资源池中获取jedis对象用于操作Redis
            Jedis jedis = JedisUtil.getJedis();
            try {
                // 获取Redis中的key剩余时间
                expire = jedis.ttl(redisKey);
            } catch (RuntimeException e) {
                e.printStackTrace();
            } finally {
                // 用完后将jedis对象返回资源池中
                JedisUtil.close(jedis);
            }

            System.out.println("[Redis TTL] <" + redisKey + "> --> " + expire);
            // 如果返回-2，说明Key不存在
            if (expire == -2) {
                System.out.println("[Redis TTL] <" + redisKey + "> --> " + expire + " (not exist)");
                return false;
            }
            // 如果返回-1，说明Key已经过了有效期
            if (expire == -1) {
                System.out.println("[Redis TTL] <" + redisKey + "> --> " + expire + " (expire)");
                return false;
            }
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 给JWT续命
     * <p>
     * 流程参考：
     * 1.将传过来的Token解析，取出subject中存储的用户ID
     * 2.根据用户ID获取Redis中存储的用户信息，如果有效且过期时间在30分钟以内，则生成新Token并自动为存储在Redis中的用户数据续期
     * 3.如果不需要续命则返回原Token，如果已续命则返回新Token
     */
    public static String continueJWT(String token) {
        try {
            // 解析JWT
            Claims claims = JWTUtil.parseJWT(token);
            // 获取Subject（UID）
            String uid = claims.getSubject();

            // 验证Redis中是否存在该Key
            String redisKey = JWTAuth.KEY_REDIS_AUTH_USER + uid;

            // Redis中的key剩余时间
            long expire = 0;
            // 从资源池中获取jedis对象用于操作Redis
            Jedis jedis = JedisUtil.getJedis();
            try {
                // 获取Redis中的key剩余时间
                expire = jedis.ttl(redisKey);
            } catch (RuntimeException e) {
                e.printStackTrace();
            } finally {
                // 用完后将jedis对象返回资源池中
                JedisUtil.close(jedis);
            }

            System.out.println("[Redis ttl ] <" + redisKey + "> --> " + expire);
            // 如果返回-2，说明Key不存在
            if (expire == -2) {
                System.out.println("[Redis ttl ] <" + redisKey + "> --> " + expire + " (not exist)");
                throw new RuntimeException("Token无效");
            }
            // 如果返回-1，说明Key已经过了有效期
            if (expire == -1) {
                System.out.println("[Redis ttl ] <" + redisKey + "> --> " + expire + " (expire)");
                throw new RuntimeException("Token过期");
            }
            // 如果失效时间小于30分钟，将重新给Key添加失效时间（简称：续命）
            if (expire <= 3) {
                long ttlMillis = 10;
                System.out.println("[Redis ttl ] <" + redisKey + "> --> ... " + ttlMillis + "(s)");
                // 创建新Token，并设置到外部调用者
                token = JWTUtil.createJWT(uid, ttlMillis);

                // 从资源池中获取jedis对象用于操作Redis
                jedis = JedisUtil.getJedis();
                try {
                    // 为Redis中的key重新设置过期时间
                    jedis.expire(redisKey, ttlMillis);
                } catch (RuntimeException e) {
                    e.printStackTrace();
                } finally {
                    // 用完后将jedis对象返回资源池中
                    JedisUtil.close(jedis);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("Token失效");
        }
        return token;
    }

    /**
     * 移除JWT（用户登录成功后生成JWT，保存用户ID，并将用户信息存储到Redis中）
     * <p>
     * 使用HS256算法，生成签名的时候使用的秘钥secret,这个方法本地封装了的，一般可以从本地配置文件中读取，切记这个秘钥不能外露。它就是你服务端的私钥，在任何场景都不应该流露出去。一旦客户端得知这个Secret, 那就意味着客户端是可以自我签发JWT了
     *
     * @param uid 授权用户ID（Security中实现了UserDetails接口的类型）
     * @return
     */
    public static boolean removeJWT(Long uid) {
        // 存放于Redis中的Key
        String redisKey = JWTAuth.KEY_REDIS_AUTH_USER + uid;

        // 从资源池中获取jedis对象用于操作Redis
        Jedis jedis = JedisUtil.getJedis();
        try {
            // 从Redis中移除用户数据
            jedis.del(redisKey);
        } catch (RuntimeException e) {
            e.printStackTrace();
        } finally {
            // 用完后将jedis对象返回资源池中
            JedisUtil.close(jedis);
        }
        return true;
    }

    public static void main(String[] args) {
        String subject = "1"; // 用户账号（ID）
        String account = "root"; // 用户名
        String realName = "LILIBO"; // 临时密码
        Map<String, Object> params = new HashMap<>();
        params.put("subject", subject);
        params.put("account", account);
        params.put("realName", realName);

        // 创建JWT
        String token = JWTAuth.generateJWT(params);
        System.out.println(" >>> " + JWTUtil.parseJWT(token).getId());

        try {
            // 验证JWTcontinueJWT
            Thread.sleep(1000);
            String newToken = continueJWT(token); // ok
            System.out.println(" >>> " + JWTUtil.parseJWT(newToken).getId());
            Thread.sleep(6 * 1000); // 等到超时
            newToken = continueJWT(token); // null
            System.out.println(" >>> " + JWTUtil.parseJWT(newToken).getId());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 验证JWT
        // String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJCb0JvIiwicGFzc3dvcmQiOiJMSUxJQk8iLCJzdWJqZWN0IjoiMSIsImlhdCI6MTYzMzA1NTgxNiwiYWNjb3VudCI6InJvb3QiLCJqdGkiOiJlMDg3NjlkMy1mYzIxLTQ2OGMtOWVjNi05NDljMGE1NjM5YTQifQ.hkyG_jOTf6ageKnM2L60tHTt94fLKjdqgrZQ9PFtYnI";
        // testVerifyJWT(token, account, password);

        // 解析JWT
        // token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJCb0JvIiwicGFzc3dvcmQiOiJMSUxJQk8iLCJzdWJqZWN0IjoiMSIsImlhdCI6MTYzMzAyNTg4OCwiYWNjb3VudCI6InJvb3QiLCJqdGkiOiJiM2EyN2ZiZC03YTYxLTQ1YjgtODczOC1kNmM4MTBhOTBhOGMifQ.jg6chO_NZmKxfH4k8CbXS4x5XMbIbnKcAsq3DHO0EZs";
        // testParseJWT(token);
    }

}